--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit ef040a9e908027d89d3913ef1d7aca82551d2a26
Parents : 151aa4c
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-06T00:21:37-05:00
feat(App): implement unread conversations count update and update telephony settings in the UI
Changes
7 files changed, 123 insertions(+), 34 deletions(-)
Diff
diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue
index e0a07cad..f3083f48 100644
--- a/meshchatx/src/frontend/components/App.vue
+++ b/meshchatx/src/frontend/components/App.vue
@@ -685,6 +685,7 @@ export default {
ElectronUtils,
reloadInterval: null,
appInfoInterval: null,
+ unreadCountInterval: null,
isShowingMyIdentitySection: true,
isShowingAnnounceSection: true,
@@ -915,6 +916,10 @@ export default {
this.appInfoInterval = setInterval(() => {
this.getAppInfo();
}, 15000);
+ this.unreadCountInterval = setInterval(() => {
+ this.updateUnreadConversationsCount();
+ }, 5000);
+ this.updateUnreadConversationsCount();
},
stopShell() {
if (!this.shellRunning) {
@@ -925,6 +930,8 @@ export default {
this.reloadInterval = null;
clearInterval(this.appInfoInterval);
this.appInfoInterval = null;
+ clearInterval(this.unreadCountInterval);
+ this.unreadCountInterval = null;
WebSocketConnection.off("message", this.onWebsocketMessage);
WebSocketConnection.off("disconnected", this.onWsShellDisconnected);
WebSocketConnection.off("connected", this.onWsShellConnected);
@@ -1084,6 +1091,21 @@ export default {
onShowTutorialShell() {
this.$refs.tutorialModal?.show();
},
+ updateUnreadConversationsCount() {
+ if (this._unreadCountTimeout) {
+ clearTimeout(this._unreadCountTimeout);
+ }
+ this._unreadCountTimeout = setTimeout(async () => {
+ try {
+ const response = await window.api.get("/api/v1/notifications", {
+ params: { unread: true, limit: 1 },
+ });
+ GlobalState.unreadConversationsCount = response.data?.lxmf_total_unread_count ?? 0;
+ } catch (e) {
+ console.error("Failed to update unread conversations count", e);
+ }
+ }, 300);
+ },
onConfigUpdatedExternally(newConfig) {
if (!newConfig || typeof newConfig !== "object") {
return;
@@ -1144,7 +1166,9 @@ export default {
if (this.initiationStatus) {
break;
}
- NotificationUtils.showIncomingCallNotification();
+ NotificationUtils.showIncomingCallNotification(
+ json.remote_identity_name || json.remote_identity_hash
+ );
this.updateTelephoneStatus();
this.playRingtone();
break;
@@ -1181,11 +1205,13 @@ export default {
this.stopRingtone();
this.ringtonePlayer = null;
this.toneGenerator.stop();
+ NotificationUtils.cancelIncomingCallNotification();
this.updateTelephoneStatus();
break;
}
case "telephone_call_ended": {
this.stopRingtone();
+ NotificationUtils.cancelIncomingCallNotification();
this.ringtonePlayer = null;
if (this.config?.telephone_tone_generator_enabled) {
this.toneGenerator.setVolume(this.config.telephone_tone_generator_volume);
@@ -1202,6 +1228,10 @@ export default {
break;
}
+ // Update sidebar unread count so the badge appears
+ // immediately even when not on the Messages page.
+ this.updateUnreadConversationsCount();
+
// show notification for new messages if window is not focussed
// only for incoming messages from people (with content)
if (
diff --git a/meshchatx/src/frontend/components/Toast.vue b/meshchatx/src/frontend/components/Toast.vue
index 31bd5f08..dd99162e 100644
--- a/meshchatx/src/frontend/components/Toast.vue
+++ b/meshchatx/src/frontend/components/Toast.vue
@@ -2,7 +2,7 @@
<template>
<div
- class="fixed max-sm:bottom-[calc(5.75rem+env(safe-area-inset-bottom,0px))] bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:right-4 sm:translate-x-0 z-100 flex flex-col gap-2 pointer-events-none w-[calc(100%-2rem)] max-w-sm sm:w-auto sm:max-w-md"
+ class="fixed max-sm:bottom-[calc(1rem+env(safe-area-inset-bottom,0px))] bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:right-4 sm:translate-x-0 z-100 flex flex-col gap-2 pointer-events-none w-[calc(100%-2rem)] max-w-sm sm:w-auto sm:max-w-md"
>
<TransitionGroup name="toast">
<div
diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue
index 812da29f..784499f8 100644
--- a/meshchatx/src/frontend/components/call/CallPage.vue
+++ b/meshchatx/src/frontend/components/call/CallPage.vue
@@ -135,7 +135,7 @@
</div>
</div>
- <div class="relative z-10 space-y-1 mb-8">
+ <div class="relative z-10 space-y-1 mb-8 flex flex-col items-center text-center">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white truncate max-w-[280px]">
{{
(activeCall || lastCall)?.remote_identity_name ||
@@ -528,34 +528,36 @@
class="pt-2 flex flex-col items-stretch gap-4 lg:flex-row lg:items-start lg:justify-between"
>
<div class="flex min-w-0 flex-1 flex-col gap-2">
- <Toggle
- id="dnd-toggle"
- :model-value="config?.do_not_disturb_enabled"
- :label="$t('call.do_not_disturb')"
- @update:model-value="toggleDoNotDisturb"
- />
- <Toggle
- id="contacts-only-toggle"
- :model-value="config?.telephone_allow_calls_from_contacts_only"
- :label="$t('call.allow_calls_from_contacts_only')"
- @update:model-value="toggleAllowCallsFromContactsOnly"
- />
- <Toggle
- id="telephone-announce-toggle"
- :model-value="config?.telephone_announce_enabled"
- label="Announce Telephone Presence (LXST)"
- @update:model-value="toggleTelephoneAnnounceEnabled"
- />
- <div class="flex flex-col gap-1">
+ <div v-if="config?.telephone_enabled" class="flex flex-col gap-2">
+ <Toggle
+ id="dnd-toggle"
+ :model-value="config?.do_not_disturb_enabled"
+ :label="$t('call.do_not_disturb')"
+ @update:model-value="toggleDoNotDisturb"
+ />
+ <Toggle
+ id="contacts-only-toggle"
+ :model-value="config?.telephone_allow_calls_from_contacts_only"
+ :label="$t('call.allow_calls_from_contacts_only')"
+ @update:model-value="toggleAllowCallsFromContactsOnly"
+ />
<Toggle
- id="web-audio-toggle"
- :model-value="config?.telephone_web_audio_enabled"
- label="Web Audio Bridge"
- @update:model-value="onToggleWebAudio"
+ id="telephone-announce-toggle"
+ :model-value="config?.telephone_announce_enabled"
+ label="Announce Telephone Presence (LXST)"
+ @update:model-value="toggleTelephoneAnnounceEnabled"
/>
- <div class="text-xs text-gray-500 dark:text-zinc-400 px-1">
- Web audio bridge allows web/electron to hook into LXST backend for
- passing microphone and audio streams to active telephone calls.
+ <div class="flex flex-col gap-1">
+ <Toggle
+ id="web-audio-toggle"
+ :model-value="config?.telephone_web_audio_enabled"
+ label="Web Audio Bridge"
+ @update:model-value="onToggleWebAudio"
+ />
+ <div class="text-xs text-gray-500 dark:text-zinc-400 px-1">
+ Web audio bridge allows web/electron to hook into LXST backend for
+ passing microphone and audio streams to active telephone calls.
+ </div>
</div>
</div>
</div>
@@ -2715,6 +2717,11 @@ export default {
this.stopWebAudio();
return;
}
+ // Do not start web audio during voicemail
+ if (this.activeCall?.is_voicemail) {
+ this.stopWebAudio();
+ return;
+ }
if (this.activeCall && webAudioStatus.enabled) {
this.audioFrameMs = webAudioStatus.frame_ms || 60;
await this.startWebAudio();
diff --git a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
index 8480cd30..61252185 100644
--- a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
+++ b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
@@ -1937,7 +1937,7 @@ export default {
interface_discovery_whitelist: "",
interface_discovery_blacklist: "",
required_discovery_value: null,
- autoconnect_discovered_interfaces: 0,
+ autoconnect_discovered_interfaces: null,
default_bootstrap_only: true,
network_identity: "",
},
diff --git a/meshchatx/src/frontend/components/interfaces/Interface.vue b/meshchatx/src/frontend/components/interfaces/Interface.vue
index e58a22c4..28441483 100644
--- a/meshchatx/src/frontend/components/interfaces/Interface.vue
+++ b/meshchatx/src/frontend/components/interfaces/Interface.vue
@@ -293,7 +293,9 @@ export default {
}
const st = this.iface._stats;
if (!st || typeof st !== "object") {
- return null;
+ // If stats are missing, the interface is likely detached or
+ // not yet initialised; show down rather than unknown.
+ return false;
}
if ("status" in st) {
const s = st.status;
diff --git a/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue b/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue
index 55c3ba8e..27d29f73 100644
--- a/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue
+++ b/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue
@@ -752,7 +752,7 @@ export default {
interface_discovery_whitelist: "",
interface_discovery_blacklist: "",
required_discovery_value: null,
- autoconnect_discovered_interfaces: 0,
+ autoconnect_discovered_interfaces: null,
default_bootstrap_only: true,
network_identity: "",
},
@@ -1242,7 +1242,7 @@ export default {
discovery.autoconnect_discovered_interfaces !== null &&
discovery.autoconnect_discovered_interfaces !== ""
? Number(discovery.autoconnect_discovered_interfaces)
- : 0;
+ : null;
this.discoveryConfig.default_bootstrap_only = this.parseBool(discovery.default_bootstrap_only ?? true);
this.discoveryConfig.network_identity = discovery.network_identity ?? "";
} catch (e) {
@@ -1266,7 +1266,7 @@ export default {
autoconnect_discovered_interfaces:
this.discoveryConfig.autoconnect_discovered_interfaces === null ||
this.discoveryConfig.autoconnect_discovered_interfaces === ""
- ? 0
+ ? null
: Number(this.discoveryConfig.autoconnect_discovered_interfaces),
default_bootstrap_only: this.discoveryConfig.default_bootstrap_only,
network_identity: this.discoveryConfig.network_identity || null,
diff --git a/meshchatx/src/frontend/components/settings/SettingsPage.vue b/meshchatx/src/frontend/components/settings/SettingsPage.vue
index 3b8fa7d5..1a1f70be 100644
--- a/meshchatx/src/frontend/components/settings/SettingsPage.vue
+++ b/meshchatx/src/frontend/components/settings/SettingsPage.vue
@@ -671,6 +671,36 @@
</div>
</section>
+ <!-- Telephony Settings -->
+ <section
+ v-show="matchesSearch(...sectionKeywords.telephony)"
+ class="settings-section break-inside-avoid"
+ >
+ <header class="settings-section__header">
+ <div>
+ <div class="settings-section__eyebrow">Telephony</div>
+ <h2>Telephone (LXST)</h2>
+ <p>Enable or disable the integrated voice calling system.</p>
+ </div>
+ </header>
+ <div class="settings-section__body space-y-4">
+ <label class="setting-toggle">
+ <Toggle
+ id="telephone-enabled-toggle"
+ v-model="config.telephone_enabled"
+ @update:model-value="onTelephoneEnabledChange"
+ />
+ <span class="setting-toggle__label">
+ <span class="setting-toggle__title">Enable Telephone (LXST)</span>
+ <span class="setting-toggle__description">
+ Allow incoming and outgoing voice calls over the mesh network.
+ </span>
+ <span class="setting-toggle__hint">Disabling will end any active calls.</span>
+ </span>
+ </label>
+ </div>
+ </section>
+
<!-- Desktop / Electron Settings -->
<section
v-if="ElectronUtils.isElectron()"
@@ -2669,6 +2699,16 @@ export default {
visualiserShowDisabledInterfaces: false,
visualiserShowDiscoveredInterfaces: false,
sectionKeywords: {
+ telephony: [
+ "Telephony",
+ "Telephone",
+ "LXST",
+ "Enable Telephone",
+ "voice",
+ "calling",
+ "call",
+ "mesh network",
+ ],
strangerProtection: [
"Security",
"app.stranger_protection",
@@ -3739,6 +3779,16 @@ export default {
);
}, 1000);
},
+ async onTelephoneEnabledChange(value) {
+ this.config.telephone_enabled = value;
+ try {
+ const newConfig = await patchServerConfig({ telephone_enabled: value }, window.api);
+ this.config = newConfig;
+ ToastUtils.success(value ? "Telephone enabled" : "Telephone disabled");
+ } catch {
+ ToastUtils.error("Failed to update telephone setting");
+ }
+ },
async onDesktopOpenCallsInSeparateWindowChange(value) {
this.config.desktop_open_calls_in_separate_window = value;
await this.updateConfig(
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────